<?php

namespace pmvc\ioc;

use pmvc\util\ReflectionUtil;

use pmvc\util\StringUtil;

use pmvc\annotation\AnnotationUtil;
use pmvc\ioc\annotation\AutowireFailedException;
use pmvc\ioc\annotation\Autowire;
use pmvc\ioc\annotation\Property;
use pmvc\ioc\annotation\Object;
use pmvc\ioc\ObjectDescriptor;
use pmvc\ioc\PropertyDescriptor;

use pmvc\ioc\ObjectFactory;

use Exception;
use ReflectionClass;
use ReflectionProperty;
use ReflectionMethod;

/**
 * Utilities for interacting with {@link ObjectFactory}s.
 */
class ObjectFactoryUtil {

	/**
	 * Returns {@link ObjectDescriptor}s for objects in the
	 * given {@link ObjectFactory} that have at least one
	 * of the given {@code $annotations}.
	 * @param ObjectFactory $objectFactory
	 * @param array $annotations
	 * @param bool $recurse
	 * @return array of ObjectDescriptor
	 */
	public static function getDescriptorsByClassAnnotations(
		ObjectFactory $objectFactory, Array $annotations, $recurse=false) {

		// collect the descriptors
		$ret = Array();
		$objectFactory->visitDescriptors(function($desc, $get)use($annotations, &$ret) {

			// get the annotations
			if (!isset($desc->class) || empty($desc->class)) {
				return;
			}
			$annots = AnnotationUtil::getClassAnnotations($desc->class, $annotations);
			if (empty($annots)) {
				return;
			}

			// add to return
			$ret []= $desc;
		});

		// return it
		return $ret;
	}

	/**
	 * Creates an {@link ObjectDescriptor} for the given {@code $class}.
	 * 
	 * @param ObjectFactory $objectFactory
	 * @param string $class
	 * @return ObjectDescriptor
	 */
	public static function createAutowireDescriptorForClass(ObjectFactory $objectFactory, $class) {

		// create type
		$clazz = new ReflectionClass($class);

		// get Object annotation
		$objectAnnots = AnnotationUtil::getClassAnnotations($clazz, 'pmvc\ioc\annotation\Object');
		if (empty($objectAnnots)) {
			throw new AutowireFailedException(
				"Cant autowire ".$class." because it doesn't have an Object annotation");
		}
		$objectAnnot = $objectAnnots[0];
	
		// generate an id if needed
		$id = isset($objectAnnot->id)
			? $objectAnnot->id
			: uniqid('', TRUE).'::Autowire#'.$clazz->getName();

		// create the descriptor
		$desc = new ObjectDescriptor($id, $class, $objectAnnot->name);
		$desc->abstract			= false;
		$desc->initMethod		= $objectAnnot->initMethod;
		$desc->destroyMethod	= $objectAnnot->destroyMethod;
		$desc->lazy 			= $objectAnnot->lazy;
		$desc->singleton		= $objectAnnot->singleton;
		$desc->parent			= $objectAnnot->parent;
		$desc->preloadFile		= $objectAnnot->preloadFile;

		// methods
		foreach ($clazz->getMethods() as $method) {

			// Autowire
			$annots = AnnotationUtil::getMethodAnnotations($method, 'pmvc\ioc\annotation\Autowire');
			if (!empty($annots)) {
				$desc->properties []= self::createAutowireDescriptorForMethod(
					$objectFactory, $method, $annots[0]);
			}

			// Property
			$annots = AnnotationUtil::getMethodAnnotations($method, 'pmvc\ioc\annotation\Property');
			if (!empty($annots)) {
				$desc->properties []= self::createPropertyDescriptorForMethod(
					$objectFactory, $method, $annots[0]);
			}
		}

		// properties
		foreach ($clazz->getProperties() as $property) {

			// Autowire
			$annots = AnnotationUtil::getPropertyAnnotations($property, 'pmvc\ioc\annotation\Autowire');
			if (!empty($annots)) {
				$desc->properties []= self::createAutowireDescriptorForProperty(
					$objectFactory, $property, $annots[0]);
			}

			// Property
			$annots = AnnotationUtil::getPropertyAnnotations($property, 'pmvc\ioc\annotation\Property');
			if (!empty($annots)) {
				$desc->properties []= self::createPropertyDescriptorForProperty(
					$objectFactory, $property, $annots[0]);
			}
		}

		// return it
		return $desc;
	}

	/**
	 * Creates a {@link PropertyDescriptor} for the given
	 * property using the given {@link Property} annotation.
	 * 
	 * @param ObjectFactory $objectFactory
	 * @param ReflectionProperty $property
	 * @param Property $annot
	 * @throws AutowireFailedException on error
	 * @return PropertyDescriptor
	 */
	public static function createPropertyDescriptorForProperty(
		ObjectFactory $objectFactory, ReflectionProperty $property, Property $annot) {
		return new PropertyDescriptor(
			$property->getName(), $annot->value, isset($annot->type) ? $annot->type : Property::STRING);
	}

	/**
	 * Creates a {@link PropertyDescriptor} for the given
	 * method using the given {@link Property} annotation.
	 * 
	 * @param ObjectFactory $objectFactory
	 * @param ReflectionMethod $method
	 * @param Property $annot
	 * @throws AutowireFailedException on error
	 * @return PropertyDescriptor
	 */
	public static function createPropertyDescriptorForMethod(
		ObjectFactory $objectFactory, ReflectionMethod $method, Property $annot) {
		
		// validate the method
		if (!StringUtil::startsWith($method->getName(), "set")
			|| count($method->getNumberOfParameters())!=1) {
			throw AutowireFailedException(
				"Property methods must start with the word 'set' "
				."and must take exactly one argument");
		}

		// the name\value
		return new PropertyDescriptor(
			substr($method->getName(), 3), $annot->value, isset($annot->type) ? $annot->type : Property::STRING);
	}
		

	/**
	 * Creates a {@link PropertyDescriptor} for the given
	 * method based on it's autowiring annotations.
	 * 
	 * @param ObjectFactory $objectFactory
	 * @param ReflectionMethod $method
	 * @param Autowire $autowire
	 * @throws AutowireFailedException on error
	 * @return PropertyDescriptor
	 */
	public static function createAutowireDescriptorForProperty(
		ObjectFactory $objectFactory, ReflectionProperty $property, Autowire $autowire) {

		// the name\value
		$pDescName 	= $property->getName();
		$pDescValue	= null;
		
		// id
		if (isset($autowire->idRef)) {
			$refDesc = $objectFactory->getDescriptorById($autowire->idRef);
			if (!isset($refDesc)) {
				throw new AutowireFailedException(
					"Cant find object with id ".$autowire->idRef." for property "
					.$property->getName()." of ".$property->getDeclaringClass());
			}
			$pDescValue = $refDesc->id;
	
		// name
		} else if (isset($autowire->nameRef)) {
			$refDesc = $objectFactory->getDescriptorByName($autowire->nameRef);
			if (!isset($refDesc)) {
				throw new AutowireFailedException(
					"Cant find object with name ".$autowire->nameRef." for property "
					.$property->getName()." of ".$property->getDeclaringClass());
			}
			$pDescValue = $refDesc->id;
	
		// type
		} else if (isset($autowire->typeRef)) {
			$descriptors = $objectFactory->getDescriptorsOfType($autowire->typeRef);
			if (empty($descriptors)) {
				throw new AutowireFailedException(
					"Cant find object of type ".$autowire->typeRef." for property "
					.$property->getName()." of ".$property->getDeclaringClass());
			}
			$pDescValue = $descriptors[0]->id;

		// property name -> object name
		} else if ($objectFactory->hasObjectWithName($property->getName())) {
			$desc = $objectFactory->getDescriptorByName($property->getName());
			$pDescValue = $desc->id;

		// property name -> object id
		} else if ($objectFactory->hasObjectWithId($property->getName())) {
			$desc = $objectFactory->getDescriptorById($property->getName());
			$pDescValue = $desc->id;

		// fail
		} else {
			throw new AutowireFailedException(
				"No autowire candidates found for property "
				.$property->getName()." of ".$property->getDeclaringClass());
		}

		// create descriptor
		return new PropertyDescriptor(
			$pDescName, $pDescValue, PropertyDescriptor::TYPE_REF);
	}

	/**
	 * Creates a {@link PropertyDescriptor} for the given
	 * method based on it's autowiring annotations.
	 * 
	 * @param ObjectFactory $objectFactory
	 * @param ReflectionMethod $method
	 * @param Autowire $autowire
	 * @throws AutowireFailedException on error
	 * @return PropertyDescriptor
	 */
	public static function createAutowireDescriptorForMethod(
		ObjectFactory $objectFactory, ReflectionMethod $method, Autowire $autowire) {
		
		// validate the method
		if (!StringUtil::startsWith($method->getName(), "set")
			|| count($method->getNumberOfParameters())!=1) {
			throw AutowireFailedException(
				"Autowire methods must start with the word 'set' "
				."and must take exactly one argument");
		}

		// the name\value
		$pDescName 	= substr($method->getName(), 3);
		$pDescValue	= null;
		$params 	= $method->getParameters();
		$param		= $params[0];
		$paramClass	= $param->getClass();
		
		// id
		if (isset($autowire->idRef)) {
			$refDesc = $objectFactory->getDescriptorById($autowire->idRef);
			if (!isset($refDesc)) {
				throw new AutowireFailedException(
					"Cant find object with id ".$autowire->idRef." for method "
					.$method->getName()." of ".$method->getDeclaringClass());
			}
			$pDescValue = $refDesc->id;
	
		// name
		} else if (isset($autowire->nameRef)) {
			$refDesc = $objectFactory->getDescriptorByName($autowire->nameRef);
			if (!isset($refDesc)) {
				throw new AutowireFailedException(
					"Cant find object with name ".$autowire->nameRef." for method "
					.$method->getName()." of ".$method->getDeclaringClass());
			}
			$pDescValue = $refDesc->id;
	
		// type
		} else if (isset($autowire->typeRef)) {
			$descriptors = $objectFactory->getDescriptorsOfType($autowire->typeRef);
			if (empty($descriptors)) {
				throw new AutowireFailedException(
					"Cant find object of type ".$autowire->typeRef." for method "
					.$method->getName()." of ".$method->getDeclaringClass());
			}
			$pDescValue = $descriptors[0]->id;
	
		// type #2
		} else if (isset($paramClass)) {
			$descriptors = $objectFactory->getDescriptorsOfType($paramClass->getName());
			if (empty($descriptors)) {
				throw new AutowireFailedException(
					"Cant find object of type ".$paramClass->getName()." for method "
					.$method->getName()." of ".$method->getDeclaringClass());
			}
			$pDescValue = $descriptors[0]->id;

		}
		
		// param name -> object name
		if ($objectFactory->hasObjectWithName($param->getName())) {
			$desc = $objectFactory->getDescriptorByName($param->getName());
			if (!isset($paramClass) || (
				isset($paramClass) && ReflectionUtil::classIsA($desc->class, $paramClass->getName()))) {
				$pDescValue = $desc->id;
			}
		}

		// param name -> object id
		if (!isset($pDescValue) && $objectFactory->hasObjectWithId($param->getName())) {
			$desc = $objectFactory->getDescriptorById($param->getName());
			if (!isset($paramClass) || (
				isset($paramClass) && ReflectionUtil::classIsA($desc->class, $paramClass->getName()))) {
				$pDescValue = $desc->id;
			}
		}

		// fail
		if (!isset($pDescValue)) {
			throw new AutowireFailedException(
				"No autowire candidates found for method "
				.$method->getName()." of ".$method->getDeclaringClass());
		}

		// create descriptor
		return new PropertyDescriptor(
			$pDescName, $pDescValue, PropertyDescriptor::TYPE_REF);
	}

}

?>